/**
 * 测试用例JSON格式导出
 *
 * Created by snowingsea on 2021/01/20.
 */
const fs = require('fs-extra');
const path = require('path');
const compressing = require('compressing');
const BaseTestCasesExporter = require('./../base-exporter');
const common = require('./../../../common');
const functionModels = require('../../../../models/functions');
const tests = require('../../../../models/tests');
const fileElements = require('../../../../models/file-elements');
const invokeRelations = require('../../../../models/invoke-relations');
const breakPoints = require('../../../../models/break-points');
const utility = require('../../../utility');

class JsonTestCasesExporter extends BaseTestCasesExporter {
  constructor(versionId, locale, version = null, project = null, creator = null, fileIds = null, exportFuncFile) {
    super(versionId, locale, version, project, creator, fileIds);
    this.exportFuncFile = exportFuncFile;
    this.versionBreakPoints = {};
    this.versionTestFieldConfig = new Map();
  }

  async exportTestCases(progressIndicator) {
    const result = {
      cachePath: this.outputDir,
    };
    await this.handleTestFieldConfig();
    await this.handleBreakPoints();
    await this.spliteByFile(null, progressIndicator);
    const fileName = `${this.project.projectName}-${this.version.versionName}.test-cases.srt`;
    result.testsPath = path.join(this.outputDir, fileName);
    const testsDir = path.join(this.outputDir, this.project.projectName);
    await compressing.zip.compressDir(testsDir, result.testsPath);
    return result;
  }

  async handleTestFieldConfig() {
    const testFieldConfigs = await tests.getTestFieldConfigByVerisonId(this.versionId);
    testFieldConfigs.forEach((testFieldConfig) => {
      this.versionTestFieldConfig.set(testFieldConfig.functionId.toString(), testFieldConfig);
    });
  }

  handleNumericalBasesKey(numericalBases) {
    const newNumericalBases = {};
    if (utility.isEmpty(numericalBases)) {
      return newNumericalBases;
    }
    Object.keys(numericalBases).forEach((numericalBaseKey) => {
      let newNumericalBaseKey = numericalBaseKey.replace(/\./g, '|');
      let objectIds = numericalBaseKey.match(/([0-9a-fA_F]{24})(?=\.)/g);
      if (objectIds) {
        objectIds = utility.arrayDeduplicate(objectIds);
        objectIds.forEach((originId) => {
          let newId;
          if (this.allFileIdsOfVersion.has(originId)) {
            newId = `$${this.allFileIdsOfVersion.get(originId).path}$`;
          }
          if (this.allFuncsOfVersion.has(originId)) {
            const func = this.allFuncsOfVersion.get(originId);
            const file = this.allFileIdsOfVersion.get(func.fileId.toString());
            newId = `$${file.path}@${func.mangledId}$`;
          }
          newNumericalBaseKey = newNumericalBaseKey.replace(new RegExp(originId, 'g'), newId);
        });
      }
      newNumericalBases[newNumericalBaseKey] = numericalBases[numericalBaseKey];
    });
    return newNumericalBases;
  }

  async handleBreakPoints() {
    const bps = await breakPoints.getBreakPointsByVersionId(this.versionId);
    bps.forEach((dp) => {
      const testcaseId = dp.testcaseId.toString();
      if (!this.versionBreakPoints[testcaseId]) {
        this.versionBreakPoints[testcaseId] = [];
      }
      delete dp._id;
      delete dp.testcaseId;
      delete dp.functionId;
      delete dp.entryFunctionId;
      delete dp.fileId;
      this.versionBreakPoints[testcaseId].push(dp);
    });
  }

  /**
  *  获取函数名
  * @param {String} functionName 函数名
  */
  getFunctonName(functionName) {
    const regExpr = /(.*)\(.*\)/;
    if (functionName.match(regExpr)) {
      [, functionName] = functionName.match(regExpr);
    }
    return functionName;
  }

  /**
   * 按文件分割
   * @return {Promise}
   */
  spliteByFile(callback = null, progressIndicator) {
    return new Promise((resolve, reject) => {
      const result = {};
      this.spliteByProjectVersion((versionDir) => fs.ensureDir(versionDir))
        .then(({ projectDir, versionPath, versionType }) => {
          result.projectDir = projectDir;
          result.versionPath = versionPath;
          result.versionDir = versionPath;
          result.versionType = versionType;
          return this.filterSelectedFiles();
        })
        .then(() => this.getFuncsByVersionId())
        .then(() => this.getFilesByVersionId())
        .then(() => this.getAllFunctionVariablesStubsByVersionId())
        .then(() => this.createVersionInfoFile(result.projectDir, result.versionType, 'json'))
        .then(() => this.handleFiles(this.fileIds, result.versionDir, callback, progressIndicator))
        .then(() => resolve(result))
        .catch(reject);
    });
  }

  /**
   * 创建导出versionInfo配置文件
   * @param {string} projectDir 项目地址
   * @param {string} versionType 版本类型
   * @param {string} exportFormat 导出格式
   * @returns
   */
  createVersionInfoFile(projectDir, versionType, exportFormat) {
    return new Promise((resolve, reject) => {
      const versionInfoFile = path.join(projectDir, 'versionInfo.json');
      fs.pathExists(versionInfoFile)
        .then((file) => {
          if (!file) {
            return fs.outputJson(versionInfoFile, { versionType, exportFormat, exportFuncFile: this.exportFuncFile });
          }
          return Promise.resolve();
        })
        .then(resolve)
        .catch(reject);
    });
  }

  /**
   * 处理单个文件，导出测试用例
   * @param {Object}} file 单个文件信息
   * @param {String} versionDir 版本目录
   * @returns {Promise}
   */
  handleFile(file, versionDir) {
    return new Promise((resolve, reject) => {
      let testsPath = `${path.join(versionDir, file.path)}`;
      if (!this.exportFuncFile) {
        testsPath += '.json';
      }
      super.handleFile(file, versionDir)
        .then((res) => {
          const { typeSystem, functions, resultCount } = res;
          if (!resultCount) {
            return Promise.resolve();
          }
          let filterFuncs = [];
          if (file.functions && file.functions.length > 0) {
            filterFuncs = functions.filter((func) => file.functions.includes(func._id.toString()));
          } else {
            filterFuncs = functions;
          }
          const isTestableFunctions = filterFuncs.filter((func) => {
            if (!fileElements.isTestableElement(func.kind)) {
              return false;
            }
            func = func.toObject();
            return true;
          });
          return this.exportTestCasesByFile(testsPath, typeSystem, isTestableFunctions.sort((a, b) => a.index - b.index));
        })
        .then(() => {
          resolve({ testsPath });
        })
        .catch(reject);
    });
  }

  /**
   * 导出单个函数的测试用例
   * @param {String} testsPath 测试用例路径
   * @param {Object} typeSystem 类型系统
   * @param {Array} functions 函数信息列表
   * @returns {Promise}
   */
  exportTestCasesByFile(testsPath, typeSystem, functions) {
    return new Promise((resolve, reject) => {
      const { versionType } = this.version;
      common.complementByProjectVersionId(this.versionId, this.version, this.project, this.creator)
        .then(() => {
          if (this.exportFuncFile) {
            return fs.ensureDir(testsPath);
          }
          return fs.ensureDir(path.dirname(testsPath));
        })
        .then(() => {
          const promises = [];
          // 构建集成测试测试用例variables
          if (this.isIntegrationTest()) {
            functions.forEach((func) => {
              promises.push(this.compositeIntegrationTestFuncVariable(func));
              promises.push(this.compositeIntegrationInvokeRelation(func));
            });
          }
          return Promise.all(promises);
        })
        .then(() => {
          const testsData = {};
          const funcNameSet = new Map();
          const promises = [];
          functions.forEach((func) => {
            const { mangledId, kind } = func;
            let functionName = this.getFunctonName(func.functionName);
            let index = 0;
            if (funcNameSet.has(functionName)) {
              index = funcNameSet.get(functionName).index + 1;
              functionName = `${functionName}_${index}`;
            }
            funcNameSet.set(this.getFunctonName(func.functionName), { index });
            if (!fileElements.isTestableElement(kind)) {
              return;
            }
            if (!this.isIntegrationTest()) {
              func.variables = func.variables ? func.variables.toObject({ minimize: false }) : {};
              Object.keys(func.variables).forEach((key) => {
                if (['_id', '__v', 'fileId', 'versionId', 'projectId', 'functionId', 'resultKind'].includes(key)) {
                  delete func.variables[key];
                }
              });
            } else {
              tests.replaceFileIdOrFuncIdToPath(
                func.variables,
                this.allFuncsOfVersion,
                this.allFileIdsOfVersion,
              );
            }
            // 死循环次数添加mangledId
            if (func.variables.infiniteLoops) {
              func.variables.infiniteLoops.forEach((loop) => {
                if (!this.allFileIdsOfVersion.has(loop.fileId.toString())
                  || !this.allFuncsOfVersion.has(loop.functionId.toString())) {
                  return;
                }
                loop.mangledId = this.allFuncsOfVersion.get(loop.functionId.toString()).mangledId;
                delete loop.fileId;
                delete loop.functionId;
              });
            }
            testsData[mangledId] = {
              functionVariables: func.variables,
              functionDesc: func.description,
              functionUdf: func.userDefinedFields,
              invokeRelations: func.invokeRelations,
              testCases: [],
              testCaseGroups: func.testCaseGroups || [],
            };
            if (this.versionTestFieldConfig.has(func._id.toString())) {
              testsData[mangledId].testFieldConfig = {
                numericalBases: this.handleNumericalBasesKey(this.versionTestFieldConfig.get(func._id.toString()).numericalBases),
              };
            }
            const funcTestcases = func.tests.sort((a, b) => a.identifierNumber - b.identifierNumber);
            funcTestcases.forEach((test) => {
              if (test.kind !== tests.testKind.modified && test.kind && this.i18n) {
                test.description = this.i18n.__('tests.structuralCoverageCase'); // eslint-disable-line no-underscore-dangle
              }
              // 只取用例可用数据
              const newTestCase = {
                _id: test.originalImportedId || test._id,
                decisions: test.decisions,
                execCount: test.execCount,
                data: test.data || {},
                description: test.description,
                requirementInfo: test.requirementInfo,
                identifierNumber: test.identifierNumber,
                userDefinedFields: test.userDefinedFields,
                versionType,
                typeName: test.typeName,
                testMethod: test.testMethod,
                breakPoints: this.versionBreakPoints[test._id.toString()],
              };
              if (JSON.stringify(newTestCase.data) === '{}') {
                testsData[mangledId].testCases.push(newTestCase);
                return;
              }
              if (this.isIntegrationTest()) { // 集成测试
                tests.replaceFileIdOrFuncIdToPath(
                  newTestCase,
                  this.allFuncsOfVersion,
                  this.allFileIdsOfVersion,
                  test.fileId.toString()
                );
                // 兼容测试用例老数据：清除测试用例下的桩函数老文件数据信息，只保留测试用例的值
                if (newTestCase.data && newTestCase.data.stubs) {
                  const { stubs } = newTestCase.data;
                  Object.keys(stubs).forEach((stubName) => {
                    delete stubs[stubName].fileId;
                    delete stubs[stubName].filePath;
                  });
                }
              }
              utility.removeObjectBykey(test.data, ['@result', '@isEqual', '@isChanged']);
              if (test.data && test.data.output && test.data.output.stubs) {
                utility.removeObjectBykey(test.data, ['@count']);
              }
              utility.removeNullItem(newTestCase);
              testsData[mangledId].testCases.push(newTestCase);
            });
            if (this.exportFuncFile) {
              promises.push(fs.writeJson(
                path.join(testsPath, `${functionName}.json`),
                { [mangledId]: testsData[mangledId] },
                { spaces: 2 }
              ));
            }
          });
          if (!this.exportFuncFile) {
            promises.push(fs.writeJson(testsPath, testsData, { spaces: 2 }));
          }
          return Promise.all(promises);
        })
        .then(resolve)
        .catch(reject);
    });
  }

  /**
   * 重新构建集成函数variables
   * @param {object} func 函数
   * @returns
   */
  compositeIntegrationTestFuncVariable(func) {
    return new Promise((resolve, reject) => {
      functionModels.getRelationFuncsVariables(func._id, {
        allFileMap: this.allFileIdsOfVersion,
        allFuncMap: this.allFuncsOfVersion,
        allFuncVariableStubs: this.allFuncVariableStubs,
      })
        .then((variables) => {
          func.variables = variables;
        })
        .then(resolve)
        .catch(reject);
    });
  }

  /**
   * 重新构建集成函数调用关系
   * @param {object} func 函数
   * @returns
   */
  compositeIntegrationInvokeRelation(func) {
    return new Promise((resolve, reject) => {
      invokeRelations.retrieveRelations(func._id)
        .then((relations) => {
          const exportRelations = [];
          relations.forEach((relation) => {
            exportRelations.push({
              fromMangled: relation.fromFuncMangledId,
              fromFilePath: this.allFileIdsOfVersion.get(relation.fromFuncFileId.toString()).path,
              toMangled: relation.toFuncMangledId,
              toFilePath: relation.toFuncFileId && this.allFileIdsOfVersion.has(relation.toFuncFileId.toString()) ? this.allFileIdsOfVersion.get(relation.toFuncFileId.toString()).path : '',
              enableStub: relation.enableStub,
            });
          });
          func.invokeRelations = exportRelations;
        })
        .then(resolve)
        .catch(reject);
    });
  }
}

/**
 * Export model definition object.
 */
module.exports = JsonTestCasesExporter;
